
// ===============================================================================================================
// -*- C++ -*-
//
// Renderer.cpp - Implementation of the Renderer singleton class.
//
// Copyright (c) 2011 Guilherme R. Lampert
// guilherme.ronaldo.lampert@gmail.com
//
// This code is licenced under the MIT license.
//
// This software is provided "as is" without express or implied
// warranties. You may freely copy and compile this source into
// applications you distribute provided that the copyright text
// above is included in the resulting source code.
//
// ===============================================================================================================

#include <Renderer.hpp>
#include <Camera.hpp>

// Objects we can render
#include <Billboard.hpp>
#include <Doom 3 MD5.hpp>
#include <ProceduralTerrain.hpp>

// Static class instance
Renderer * Renderer::singleton = 0;

// Static class data
int Renderer::videoResolutionX = 0;
int Renderer::videoResolutionY = 0;

// Set some default values:
double Renderer::FOV = 60.0;
double Renderer::zFar = 3500.0;
double Renderer::zNear = 0.3;

// =========================================================
// Renderer Class Implementation
// =========================================================

Renderer::Renderer(void)
: loadedTextures(), deferredImages(), deferredStrings(), deferredBillboards(), fadeScreen(false), currentCamera(0), playerWeapon(0), weaponTransform(0)
{
	// Init internal stuff...
}

Renderer::~Renderer(void)
{
	TexturesTable::const_iterator it = loadedTextures.begin();
	TexturesTable::const_iterator end = loadedTextures.end();

	for (; it != end; ++it)
	{
		// Destroy this texture:
		while ((*it).second->Release() > 0)
			;
	}
}

void Renderer::SwitchToFullScreen(void) const
{
	glutFullScreen();
}

void Renderer::BeginScene(unsigned int flags)
{
	glClear(flags);
	glLoadIdentity();
}

void Renderer::EndScene(void)
{
	// 3D Rendering Done Here //

	if (!deferredBillboards.empty() && (currentCamera != 0)) // Must have a valid camera!
	{
		// Render the billboards:

		glPushMatrix();

		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		glEnable(GL_ALPHA_TEST);
		glAlphaFunc(GL_GREATER, 0);

		while (!deferredBillboards.empty())
		{
			const Billboard * defBB = deferredBillboards.front();

			glBindTexture(GL_TEXTURE_2D, defBB->textureID);

			// Check the distance of the billboard from the camera,
			// this will disable animation on billboard far away from the viewer, saving some CPU cycles.

			const Vec3 up(currentCamera->GetUp());
			const Vec3 eye(currentCamera->GetEye());
			const Vec3 right(currentCamera->GetRight());

			const Vec3 dist((eye.x - defBB->pos.x), (eye.y - defBB->pos.y), (eye.z - defBB->pos.z));
			const float absDist = dist.Length();

			if (absDist < defBB->distForAnimToPlay)
			{
				// Close to camera, animate:
				const Image * img = defBB->sprite->GetCurrentFrame();
				MemoryBuffer * pixels = img->Pixels();

				// Update The Texture:
				glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, img->Width(), img->Height(), img->Format(), GL_UNSIGNED_BYTE, pixels->GetBufferPointer());
				pixels->Release();
			}

			if (absDist < 1000.0f) // Visible ?
			{
				glBegin(GL_QUADS);
				glTexCoord2f(0.0f, 0.0f);
				glVertex3fv(Vec3(defBB->pos.x + (right.x + up.x) * -defBB->size,
				                 defBB->pos.y + (right.y + up.y) * -defBB->size,
				                 defBB->pos.z + (right.z + up.z) * -defBB->size).v);

				glTexCoord2f(1.0f, 0.0f);
				glVertex3fv(Vec3(defBB->pos.x + (right.x - up.x) * defBB->size,
				                 defBB->pos.y + (right.y - up.y) * defBB->size,
				                 defBB->pos.z + (right.z - up.z) * defBB->size).v);

				glTexCoord2f(1.0f, 1.0f);
				glVertex3fv(Vec3(defBB->pos.x + (right.x + up.x) * defBB->size,
				                 defBB->pos.y + (right.y + up.y) * defBB->size,
				                 defBB->pos.z + (right.z + up.z) * defBB->size).v);

				glTexCoord2f(0.0f, 1.0f);
				glVertex3fv(Vec3(defBB->pos.x + (up.x - right.x) * defBB->size,
				                 defBB->pos.y + (up.y - right.y) * defBB->size,
				                 defBB->pos.z + (up.z - right.z) * defBB->size).v);
				glEnd();
			}

			defBB->Release();
			deferredBillboards.pop();
		}

		glDisable(GL_ALPHA);
		glDisable(GL_BLEND);

		glPopMatrix();
	}

	// Done with the common 3D stuff, we disable the z-buffer and render the player weapon, if any:

	if (playerWeapon != 0)
	{
		// This old school z-buffer trick makes very easy to position the weapon:
		glLoadIdentity();
		glClear(GL_DEPTH_BUFFER_BIT);

		DrawDoomMD5Model(playerWeapon, weaponTransform);

		playerWeapon->Release();
		playerWeapon = 0;
	}

	// 2D Rendering Done Here //

	if (!deferredImages.empty() || !deferredStrings.empty() || fadeScreen)
	{
		// If we have any 2D rendering to do:

		glBegin2D(videoResolutionX, videoResolutionY);

		// First draw the text strings:

		while (!deferredStrings.empty())
		{
			const DeferredString & defStr = deferredStrings.front();

			glColor4ubv(reinterpret_cast<const GLubyte *>(&defStr.color));
			glRasterPos2i(defStr.x, defStr.y);

			for (const char * p = defStr.text; *p != '\0'; ++p)
			{
				// FIXME: I hardcoded the font type to GLUT_BITMAP_HELVETICA_18. We should allow the user to set this in the future.
				glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, *p);
			}

			deferredStrings.pop();
		}

		// Now render the images:

		while (!deferredImages.empty())
		{
			const DeferredImage & defImg = deferredImages.front();

			glRasterPos2i(defImg.x, defImg.y);

			const MemoryBuffer * px = defImg.image->Pixels();
			glDrawPixels(defImg.image->Width(), defImg.image->Height(), defImg.image->Format(), GL_UNSIGNED_BYTE, px->GetBufferPointer());
			px->Release();

			defImg.image->Release(); // Release the image and pop the queue
			deferredImages.pop();
		}

		// Lastly, fade the screen with a user defined color, if needed:

		if (fadeScreen)
		{
			glColor4ubv(reinterpret_cast<const GLubyte *>(&fadeColor));

			glBegin(GL_QUADS);
			glVertex2i(0,0);
			glVertex2i(videoResolutionX, 0);
			glVertex2i(videoResolutionX, videoResolutionY);
			glVertex2i(0, videoResolutionX);
			glEnd();

			fadeScreen = false;
		}

		glEnd2D();
	}

	glutSwapBuffers();
	glutPostRedisplay();
}

void Renderer::SetCamera(const Camera * cam)
{
	currentCamera = cam;

	if (currentCamera != 0)
	{
		const Vec3 up(currentCamera->GetUp());
		const Vec3 eye(currentCamera->GetEye());
		const Vec3 target(currentCamera->GetTarget());

		gluLookAt(eye.x, eye.y, eye.z, target.x, target.y, target.z, up.x, up.y, up.z);
	}
}

const Camera * Renderer::GetCamera(void) const
{
	return (currentCamera);
}

Texture * Renderer::Create2DTextureFromImage(const Image * sourceImage)
{
	TexturesTable::const_iterator it = loadedTextures.find(sourceImage->FileName());

	Texture * tex = 0;

	if (it != loadedTextures.end())
	{
		// The texture has been found, return it:
		tex = (*it).second;
		tex->AddRef();
	}
	else // Create a new texture:
	{
		try {
			tex = new GL_2D_Texture(sourceImage);

			if (tex->Fail())
			{
				tex->Release();
				tex = 0;
			}
			else
			{
				// Register the texture as a new entry:
				loadedTextures.insert(TexturesTable::value_type(tex->FileName(), tex));
				tex->AddRef();
			}
		}
		catch (...) {

			if (tex)
			{
				tex->Release();
				tex = 0;
			}
		}
	}

	return (tex);
}

Texture * Renderer::Create2DTextureFromFile(const std::string & fileName)
{
	TexturesTable::const_iterator it = loadedTextures.find(fileName);

	Texture * tex = 0;

	if (it != loadedTextures.end())
	{
		// The texture has been found, return it:
		tex = (*it).second;
		tex->AddRef();
	}
	else // Create a new texture:
	{
		try {

			ImagePtr img = ImageFactory::CreateImageFromFile(fileName);

			tex = new GL_2D_Texture(img.Get());

			if (tex->Fail())
			{
				tex->Release();
				tex = 0;
			}
			else
			{
				// Register the texture as a new entry:
				loadedTextures.insert(TexturesTable::value_type(tex->FileName(), tex));
				tex->AddRef();
			}
		}
		catch (...) {

			if (tex)
			{
				tex->Release();
				tex = 0;
			}
		}
	}

	return (tex);
}

void Renderer::ReleaseCachedTextures(void)
{
	if (!loadedTextures.empty())
	{
		TexturesTable::const_iterator it = loadedTextures.begin();
		TexturesTable::const_iterator end = loadedTextures.end();

		while (it != end)
		{
			// Release the texture, if only referenced by the Renderer.

			if ((*it).second->ReferenceCount() == 1)
			{
				// Cached texture, release it:
				(*it).second->Release();
				it = loadedTextures.erase(it);
			}
			else
			{
				++it;
			}
		}
	}
}

void Renderer::DrawImage(int x, int y, const Image * image)
{
	if (image != 0)
	{
		image->AddRef();
		DeferredImage defImg = { x, y, image };
		deferredImages.push(defImg); // Add new image to the rendering queue.
	}
}

void Renderer::DrawProgressBar(const std::string & text, const unsigned int colors[], int width, int height, float prc)
{
	const int left = (videoResolutionX >> 1) - (width  >> 1);
	const int top  = (videoResolutionY >> 1) - (height >> 1);

	Renderer::Instance()->PrintString(left, top + height + 20, colors[0], text.c_str());

	// Progress bar (immediate drawing!) //

	const GLboolean depthEnabled = glIsEnabled(GL_DEPTH_TEST);

	if (depthEnabled)
	{
		glDisable(GL_DEPTH_TEST);
	}

	glBegin2D(videoResolutionX, videoResolutionY);

	int x1 = left;
	int x2 = left + width;

	int y1 = top;
	int y2 = top + height;

	glColor4ubv(reinterpret_cast<const GLubyte *>(&colors[1]));
	glRecti(x1, y1, x2, y2);

	x1 = left + 2;
	x2 = left + width - 2;

	y1 = top + 2;
	y2 = top + height - 2;

	glColor4ubv(reinterpret_cast<const GLubyte *>(&colors[2]));
	glRecti(x1, y1, x2, y2);

	x1 = left + 2;
	x2 = x1 + (width - 4) * prc;

	y1 = top + 2;
	y2 = top + height - 2;

	glColor4ubv(reinterpret_cast<const GLubyte *>(&colors[3]));
	glRecti(x1, y1, x2, y2);

	glEnd2D();

	if (depthEnabled)
	{
		glEnable(GL_DEPTH_TEST);
	}
}

void Renderer::FadeScreen(unsigned int color)
{
	// Next time EndScene() is called we will fade the screen with 'color'.
	fadeColor = color;
	fadeScreen = true;
}

void Renderer::PrintString(int x, int y, unsigned int color, const char * format, ...)
{
	if (format != 0)
	{
		DeferredString defStr;
		va_list vaList;

		va_start(vaList, format);
		int count = vsnprintf(defStr.text, sizeof(defStr.text), format, vaList);
		va_end(vaList);

		if (count > 0)
		{
			defStr.x = x;
			defStr.y = y;
			defStr.color = color;
			deferredStrings.push(defStr); // Add new string to the rendering queue.
		}
	}
}

void Renderer::DrawDoomMD5Model(const DoomMD5Model * model, const float * transform, const Texture * normalMap, ShaderProgram * shader)
{
	if (model != 0)
	{
		if (shader != 0)
		{
			shader->Enable();
			shader->SetUniform1i("normalMapTex", 1);
		}

		glPushMatrix();

		if (transform != 0)
		{
			glMultMatrixf(transform);
		}

		glEnableClientState(GL_VERTEX_ARRAY);
		glEnableClientState(GL_TEXTURE_COORD_ARRAY);

		// Draw each mesh of the model:
		for (int i = 0; i < model->numMeshes; ++i)
		{
			if (normalMap != 0)
			{
				normalMap->Bind(1); // Hardcoded for now !!!
			}

			if (model->meshes[i].textureObject != 0)
			{
				// We should sort the meshes by texture in the future...
				model->meshes[i].textureObject->Bind(0);
			}

			// Apply a software skinning to the mesh. A good thing to do would be calculate this on the GPU.
			DoomMD5Model::PrepareMesh(&model->meshes[i], model->baseSkel);

			glTexCoordPointer(2, GL_FLOAT, sizeof(Vec2), model->texCoordArray);

			glVertexPointer(3, GL_FLOAT, sizeof(Vec3), model->vertexArray);

			glDrawElements(GL_TRIANGLES, model->meshes[i].numTris * 3, GL_UNSIGNED_INT, model->indexArray);
		}

		glDisableClientState(GL_TEXTURE_COORD_ARRAY);
		glDisableClientState(GL_VERTEX_ARRAY);

		glPopMatrix();

		if (shader != 0)
		{
			shader->Disable();
		}
	}
}

void Renderer::DrawGameLevel(const GameLevel * level, const Texture * normalMap, ShaderProgram * shader)
{
	if (level != 0)
	{
		switch (level->GetLevelType())
		{
		case GameLevel::PROCEDURAL_TERRAIN:
			{
				const ProceduralTerrain * terrainLevel = reinterpret_cast<const ProceduralTerrain *>(level);

				if (shader != 0)
				{
					shader->Enable();
					shader->SetUniform1i("normalMapTex", 1);
				}

				glPushMatrix();	

				glEnableClientState(GL_VERTEX_ARRAY);
				glEnableClientState(GL_TEXTURE_COORD_ARRAY);

				glVertexPointer(3, GL_FLOAT, 0, terrainLevel->vertexArray);
				glTexCoordPointer(2, GL_FLOAT, 0, terrainLevel->texCoordArray);

				if (normalMap != 0)
				{
					normalMap->Bind(1);
				}

				if (terrainLevel->texture != 0)
				{
					terrainLevel->texture->Bind(0);
				}

				if (glLockArraysEXT)
				{
					glLockArraysEXT(0, (terrainLevel->sizeX * terrainLevel->sizeZ * 6));
				}

				for (int z = 0; z < (terrainLevel->sizeZ - 1); ++z)
				{
					glDrawElements(GL_TRIANGLE_STRIP, (terrainLevel->sizeX * 2), GL_UNSIGNED_INT, &terrainLevel->indexArray[z * terrainLevel->sizeX * 2]);
				}

				if (glUnlockArraysEXT)
				{
					glUnlockArraysEXT();
				}

				glDisableClientState(GL_TEXTURE_COORD_ARRAY);
				glDisableClientState(GL_VERTEX_ARRAY);

				glPopMatrix();

				if (shader != 0)
				{
					shader->Disable();
				}

				// Show the level name:
				Renderer::Instance()->PrintString(10, videoResolutionY - 10, PackRGBA(50, 128, 50, 255), "Level: %s", terrainLevel->name.c_str());
				break;
			}
		default:
			{
				// Unknown game level type, report error and return:
				Renderer::Instance()->PrintString(10, videoResolutionY - 10, 0xff0000ff, "Bad GameLevel Type! Unable to render it...");
				return;
			}
		}
	}
}

void Renderer::DrawBillboard(const Billboard * billboard, float animationTimer)
{
	if (billboard != 0)
	{
		billboard->AddRef();
		billboard->sprite->GetFrame(animationTimer); // Advance animation timer
		deferredBillboards.push(billboard); // Add to queue
	}
}

void Renderer::DrawPlayerWeapon(const DoomMD5Model * weapon, const float * transform)
{
	if (weapon != 0) // Grab a reference for latter...
	{
		playerWeapon = weapon;
		playerWeapon->AddRef();
	}

	weaponTransform = transform;
}

void Renderer::DrawSkyBox(const SkyBox * pBox)
{
	glCallList(pBox->displayListID);
}

bool Renderer::Initialize(int vidWidth, int vidHeight, unsigned int miscFlags, const std::string & windowTitle)
{
	if (singleton == 0)
	{
		try {
			singleton = new Renderer;
		}
		catch (...) {
			return (false);
		}

		videoResolutionX = vidWidth;
		videoResolutionY = vidHeight;

		// Setup GLUT:

		int glutFlags = GLUT_RGBA;

		if (miscFlags & INIT_DOUBLE_BUFFERED)
		{
			glutFlags |= GLUT_DOUBLE;
		}
		if (miscFlags & INIT_DEPTH_BUFFER)
		{
			glutFlags |= GLUT_DEPTH;
		}

		int argc = 1;
		char * argv[1] = { "Autor: Guilherme R. Lampert" };
		glutInit(&argc, argv); // This will fail if we pass null pointers to it!

		glutInitDisplayMode(glutFlags);
		glutInitWindowSize(videoResolutionX, videoResolutionY);
		glutCreateWindow(windowTitle.c_str());

		glutReshapeFunc(Renderer::OnWindowResize);

		if (miscFlags & INIT_FULLSCREEN)
		{
			glutFullScreen();
		}
		// else windowed.

		if (!glInitExtensions())
		{
			// If missing extensions, alert the user and continue.
			SysWarningMessage("Some OpenGL extensions are not available! The program may not run properly.");
		}

		if (miscFlags & INIT_DEPTH_BUFFER)
		{
			glEnable(GL_DEPTH_TEST);
		}

		glEnable(GL_TEXTURE_2D);
		glShadeModel(GL_SMOOTH);

		// Start our cursor in the middle of the screen:
		SetCursorPos(GetSystemMetrics(SM_CXSCREEN) >> 1, GetSystemMetrics(SM_CYSCREEN) >> 1); 
	}

	return (true);
}

Renderer * Renderer::Instance(void)
{
	return (singleton);
}

void Renderer::Kill(void)
{
	if (singleton != 0)
	{
		delete singleton;
		singleton = 0;
	}
}

void Renderer::OnWindowResize(int w, int h)
{
	// GLUT will call this function on window resize.

	videoResolutionX = ((w > 0) ? w : 1);
	videoResolutionY = ((h > 0) ? h : 1);

	glViewport(0, 0, videoResolutionX, videoResolutionY);

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	gluPerspective(FOV, (static_cast<GLdouble>(videoResolutionX) / static_cast<GLdouble>(videoResolutionY)), zNear, zFar);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
}

// =========================================================
// SkyBox Class Implementation
// =========================================================

SkyBox::SkyBox(float x, float y, float z, float width, float height, float length, Texture * const skyBoxTextures[SKY_BOX_NUM_SIDES])
{
	displayListID = glGenLists(1);

	glNewList(displayListID, GL_COMPILE);

	// This centers the sky box around (x, y, z)
	x = x - width  / 2;
	y = y - height / 2;
	z = z - length / 2;

	skyBoxTextures[SKY_BOX_BACK]->Bind();

	glBegin(GL_QUADS);
	// Assign the texture coordinates and vertices for the BACK Side
	glTexCoord2f(1.0f, 0.0f); glVertex3f(x + width, y,			z);
	glTexCoord2f(1.0f, 1.0f); glVertex3f(x + width, y + height, z);
	glTexCoord2f(0.0f, 1.0f); glVertex3f(x,			y + height, z);
	glTexCoord2f(0.0f, 0.0f); glVertex3f(x,			y,			z);
	glEnd();

	skyBoxTextures[SKY_BOX_FRONT]->Bind();

	glBegin(GL_QUADS);
	// Assign the texture coordinates and vertices for the FRONT Side
	glTexCoord2f(1.0f, 0.0f); glVertex3f(x,			y,			z + length);
	glTexCoord2f(1.0f, 1.0f); glVertex3f(x,			y + height, z + length);
	glTexCoord2f(0.0f, 1.0f); glVertex3f(x + width, y + height, z + length);
	glTexCoord2f(0.0f, 0.0f); glVertex3f(x + width, y,			z + length);
	glEnd();

	skyBoxTextures[SKY_BOX_BOTTOM]->Bind();

	glBegin(GL_QUADS);
	// Assign the texture coordinates and vertices for the BOTTOM Side
	glTexCoord2f(1.0f, 0.0f); glVertex3f(x,			y,			z);
	glTexCoord2f(1.0f, 1.0f); glVertex3f(x,			y,			z + length);
	glTexCoord2f(0.0f, 1.0f); glVertex3f(x + width, y,			z + length);
	glTexCoord2f(0.0f, 0.0f); glVertex3f(x + width, y,			z);
	glEnd();

	skyBoxTextures[SKY_BOX_TOP]->Bind();

	glBegin(GL_QUADS);
	// Assign the texture coordinates and vertices for the TOP Side
	glTexCoord2f(0.0f, 1.0f); glVertex3f(x + width, y + height, z);
	glTexCoord2f(0.0f, 0.0f); glVertex3f(x + width, y + height, z + length);
	glTexCoord2f(1.0f, 0.0f); glVertex3f(x,			y + height,	z + length);
	glTexCoord2f(1.0f, 1.0f); glVertex3f(x,			y + height,	z);
	glEnd();

	skyBoxTextures[SKY_BOX_LEFT]->Bind();

	glBegin(GL_QUADS);
	// Assign the texture coordinates and vertices for the LEFT Side
	glTexCoord2f(1.0f, 1.0f); glVertex3f(x,			y + height,	z);
	glTexCoord2f(0.0f, 1.0f); glVertex3f(x,			y + height,	z + length);
	glTexCoord2f(0.0f, 0.0f); glVertex3f(x,			y,			z + length);
	glTexCoord2f(1.0f, 0.0f); glVertex3f(x,			y,			z);
	glEnd();

	skyBoxTextures[SKY_BOX_RIGHT]->Bind();

	glBegin(GL_QUADS);
	// Assign the texture coordinates and vertices for the RIGHT Side
	glTexCoord2f(0.0f, 0.0f); glVertex3f(x + width, y,			z);
	glTexCoord2f(1.0f, 0.0f); glVertex3f(x + width, y,			z + length);
	glTexCoord2f(1.0f, 1.0f); glVertex3f(x + width, y + height,	z + length);
	glTexCoord2f(0.0f, 1.0f); glVertex3f(x + width, y + height,	z);
	glEnd();

	glEndList();
}

SkyBox::~SkyBox(void)
{
	glDeleteLists(displayListID, 1);
}